package bancosys.tec.persist.traverse;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Transient;

import bancosys.tec.utils.traverser.AbstractTraverser;
import bancosys.tec.utils.traverser.Visitor;

/**
 * Traverser de beans.
 * 
 * @author Marcio Ribeiro (mmr)
 * @author Rodrigo Queiroz
 */
public class BeanTraverser extends AbstractTraverser {

    private final Set<Class<?>> traversableClasses = new HashSet<Class<?>>();

    /**
     * Construtor.
     */
    public BeanTraverser() {
        // vazio.
    }

    /**
     * Classe que percorre a estrutura do grafo de objetos
     * 
     * @param visitors lista de visitors a serem aplicados
     */
    public BeanTraverser(List<? extends Visitor> visitors) {
        super(visitors);

        // Adicionando collections em traversableClasses
        this.addTraversableClasses(Collection.class);
        this.addTraversableClasses(Map.class);
    }

    /**
     * Define o valor para o campo traversableClasses.
     * 
     * @param traversableClasses o valor para o campo traversableClasses.
     * @throws ClassNotFoundException caso alguma classe não exista.
     */
    public void setTraversableClasses(Set<String> traversableClasses) throws ClassNotFoundException {
        this.traversableClasses.add(Collection.class);
        this.traversableClasses.add(Map.class);

        for (String clazz : traversableClasses) {
            this.traversableClasses.add(Class.forName(clazz));
        }
    }

    /**
     * Adiciona classe possivel de ser percorrida.
     * 
     * @param clazz classe.
     */
    public void addTraversableClasses(Class<?> clazz) {
        this.traversableClasses.add(clazz);
    }

    /**
     * Devolve um Iterable de objetos relacionados com o nó.
     * 
     * @param node nó que está sendo visitado
     * @return um Iterable de objetos relacionados
     */
    @Override
    protected Iterable<Object> getLinkedNodes(Object node) {
        Collection<Object> linkedNodes;
        if (this.isCollectionType(node)) {
            linkedNodes = this.extractCollectionElements(node);
        } else {
            linkedNodes = this.extractLinkedNodes(node);
        }

        return linkedNodes;
    }

    /**
     * Devolve <code>true</code> se metodo é um getter.
     * 
     * @param method método correspondente.
     * @return <code>true</code> se metodo é um getter.
     */
    private boolean isGetter(Method method) {
        return method.getName().startsWith("get") && method.getParameterTypes().length == 0;
    }

    /**
     * Devolve <code>true</code> se metodo possui anotacao Transient.
     * 
     * @param method método correspondente.
     * @return <code>true</code> se metodo possui anotacao Transient.
     */
    private boolean isTransient(Method method) {
        return method.isAnnotationPresent(Transient.class);
    }

    /**
     * Devolve os objetos relacionados com um nó
     * 
     * @param node nó
     * @return coleção
     */
    private Collection<Object> extractLinkedNodes(Object node) {
        Collection<Object> linkedNodes = new ArrayList<Object>();

        for (Method method : node.getClass().getMethods()) {
            if (this.isGetter(method) && !this.isTransient(method)) {
                try {
                    linkedNodes.add(method.invoke(node));
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
        return linkedNodes;
    }

    /**
     * Devolve os elementos da coleção se node for uma coleção ou uma lista vazia caso não seja uma coleção
     * 
     * @param node nó
     * @return coleção que o nó representa
     */
    @SuppressWarnings("unchecked")
    private Collection<Object> extractCollectionElements(Object node) {
        Collection elements = new ArrayList();

        if (node instanceof Collection) {
            elements = (Collection) node;
        } else if (node instanceof Map) {
            Map map = (Map) node;
            elements.addAll(map.keySet());
            elements.addAll(map.values());
        } else if (node.getClass().isArray()) {
            for (int i = 0; i < Array.getLength(node); i++) {
                elements.add(Array.get(node, i));
            }
        }

        return elements;
    }

    /**
     * Método que diz se deve parar a busca nesse ponto ou não. Apenas devolve <code>true</code> se o nó já foi visitado ou se não é um
     * persistable
     * 
     * @param node nó que deve ser visitado ou não
     * @param visitedNodes conjunto de nós que já foram visitados
     * @return se deve parar a busca nesse ponto ou não
     */
    @Override
    protected boolean mustStopTraverse(Object node, Set<Object> visitedNodes) {
        if (super.mustStopTraverse(node, visitedNodes)) {
            return true;
        }

        for (Class<?> clazz : this.traversableClasses) {
            if (clazz.isAssignableFrom(node.getClass())) {
                return false;
            }
        }
        return true;
    }

}
